接著要示範如何用 Eloquent 建立多對多關聯的查詢,目標幫目前的 Todo 建立 Tag 標籤,一個 Todo 可以有多個 Tag ,一個 Tag 底下有多個 Todo。
建立多對多關聯需要多一張樞鈕資料表做中介。
首先建立對應的 Model 順便建立 migration
sail artisan make:model Tag -mcr
sail artisan make:model TodoTag
sail artisan make:migration create_todo_tag_table --create todo_tag
建立 TodoTag 時不帶入 -m 建立 migration 是因為會變成建立 todo_tags 資料表,多一個 s。
當然建立後手動改成 todo_tag 也行。
Eloquent 在查詢多對多關聯時會將兩個表單的名稱連接作為中介資料表的預設名稱,不過也可以在定義關聯時自訂中介資料表的名稱,所以不用太糾結。
Tag 的部分多加上一個 name 欄位就好
/database/migrations/XXXX_XX_XX_XXXXXX_create_tags_table.php
Schema::create('tags', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->timestamps();
});
TodoTag 則要加上兩個外鍵
/database/migrations/XXXX_XX_XX_XXXXXX_create_todo_tag_table.php
Schema::create('todo_tag', function (Blueprint $table) {
$table->id();
$table->foreignId('todo_id')->constrained();
$table->foreignId('tag_id')->constrained();
$table->timestamps();
});
/app/Models/Todo.php
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use App\Models\User;
+use App\Models\Tag;
class Todo extends Model
{
@@ -18,4 +19,10 @@ class Todo extends Model
{
return $this->belongsTo(User::class);
}
+
+ public function tags()
+ {
+ return $this->belongsToMany(Tag::class);
+
+ }
}
多對多關聯的方法是用 belongsToMany ,注意跟 hasMany 的差別。
hasMany 用於一對多,會預設對方表單有關聯自己 id 的外鍵來做查詢。
belongsToMany 則會經由中介資料表去查詢。
前面提到 belongsToMany 會預設中介資料表的名稱,不過也可以自訂。
$this->belongsToMany(Tag::class,'todo_tag');
再進一步可以自訂關聯用的欄位名稱
// belongsToMany(目標表單名稱,中介表單名稱,中介表單上參照自己的外鍵,中介表單上參照目標的外鍵)
$this->belongsToMany(Tag::class,'todo_tag','todo_id','tag_id');
// belongsToMany(目標表單名稱,中介表單名稱,中介表單上參照自己的外鍵,中介表單上參照目標的外鍵,自己的關聯鍵,目標的關聯鍵)
$this->belongsToMany(Tag::class,'todo_tag','todo_id','tag_id','id','id');
反向定義 Tag 對 Todo 的關聯是一樣的寫法
/app/Models/Tag.php
@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
+use App\Models\Todo;
class Tag extends Model
{
use HasFactory;
+ public function todos()
+ {
+ return $this->belongsToMany(Todo::class,'todo_tag');
+ }
}
當我們建立 todo_tag 表單時有加上 timeStamp 欄位,不過經由 belongsToMany 關聯建立資料時,預設是不會幫中介表單加上 timeStamp 的,也就是說 todo_tag 的 create_at 跟 update_at 會為空。
如果想要幫中介表單加上 timeStamp ,要加上 withTimeStamps 方法
public function tags()
{
return $this->belongsToMany(Tag::class,'todo_tag')->withTimestamps();
}
首先加上路由跟 addTag 方法。
/routes/web.php
@@ -31,7 +32,9 @@ Route::get('/dashboard', [TodoController::class,
+Route::post('todos/{todo}/tag', [TodoController::class,'addTag'])->name('todos.addTag');
/app/Http/Controllers/TodoController.php
@@ -110,4 +107,19 @@ class TodoController extends Controller
// Todo::whereIn('id', $todoIds )->delete();
return $count;
}
+
+ public function addTag(Request $request,Todo $todo)
+ {
+ $data = $request->all();
+
+ $tag = Tag::firstOrCreate(
+ ['name' => $data['name']]
+ );
+
+ $todo->tags()->attach($tag->id);
+ }
}
我們一樣可以用 $todo->tags()->create() 方法建立與目前 todo 關聯的 tag 資料,不過這樣很有可能就會建立 name 相同的 tag ,所以這邊將做法改為先確認是否已經有同名稱的 tag ,沒有的話就新建,有的話就讀取,然後再建立跟 todo 的關聯。
firstOrCreate 方法用於確認是否有對應查詢的資料,有的話就取出第一筆,沒有的話就根據搜尋條件建立資料,同時也能追加搜尋條件以外的資料進行新增。
$flight = Flight::firstOrCreate(
['name' => 'London to Paris'], //搜尋的條件
['delayed' => 1, 'arrival_time' => '11:30'] // 當找不到資料而進行新增時,追加的資料
);
如果不想馬上寫入資料庫,也可用 firstOrNew 方法,在找不到對應資料時先建立 Model Instance。
$flight = Flight::firstOrNew(
['name' => 'Tokyo to Sydney'],
['delayed' => 1, 'arrival_time' => '11:30']
);
當新增或查詢到目標的 tag 之後,就能用 belongsToMany 的 attach 方法建立關聯。
$todo->tags()->attach($tag->id);
注意這邊 attach 的參數是目標的 id ,也就是關聯欄位的資料,而不是整個 Model Instance。
如果傳入 id 陣列的話 attach 也能用來批次新增關聯
$todo->tags()->attach([1,2,3]);
如果要移除關聯,一樣藉由 id 進行 detach
// 移除一個關聯
$todo->tags()->detach(1);
// 移除多個關聯
$todo->tags()->detach([1,2,3]);
以此來建立移除 tag 的方法
/routes/web.php
@@ -31,7 +32,9 @@ Route::get('/dashboard', [TodoController::class,
+ Route::put('todos/{todo}/tag', [TodoController::class,'removeTag'])->name('todos.removeTag');
/app/Http/Controllers/TodoController.php
@@ -110,4 +107,24 @@ class TodoController extends Controller
+
+ public function removeTag(Request $request,Todo $todo)
+ {
+ $data = $request->all();
+ $todo->tags()->detach($data['tagId']);
+ }
}
因為是要跟著 todo 清單一起顯示的,就在讀取 todos 資料時用 with 帶上 tag 清單。
/app/Http/Controllers/TodoController.php
@@ -16,7 +17,7 @@ class TodoController extends Controller
public function index()
{
return inertia('Dashboard', [
- 'todos' => Auth::user()->todos,
+ 'todos' => Auth::user()->todos()->with('tags')->get(),
]);
}
畫面的變更部分就做參考吧,都跟之前一樣發送請求後刷新 todo 清單。
/resources/js/Pages/Dashboard.js
@@ -13,6 +13,8 @@ import {
ListItemIcon,
ListItemButton,
Checkbox,
+ Chip,
+ Stack,
} from "@mui/material";
import { Head, Link, useForm } from "@inertiajs/inertia-react";
import { Inertia } from "@inertiajs/inertia";
@@ -29,7 +31,12 @@ export default function Dashboard(props) {
);
const [todoList, setTodoList] = useState(
- todos.map((todo) => ({ ...todo, editing: false, checked: false }))
+ todos.map((todo) => ({
+ ...todo,
+ editing: false,
+ checked: false,
+ tagging: false,
+ }))
);
const handleChange = (event) => {
@@ -114,6 +121,43 @@ export default function Dashboard(props) {
);
};
+ const toggleTagging = (todoId) => {
+ const newList = todoList.map((todo) =>
+ todo.id === todoId
+ ? { ...todo, tagging: !todo.tagging }
+ : { ...todo, tagging: false }
+ );
+ setTodoList(newList);
+ };
+
+ const requestAddTag = (todoId, name) => {
+ Inertia.post(
+ route("todos.addTag", { id: todoId }),
+ {
+ name,
+ },
+ {
+ onFinish: (visit) => {
+ Inertia.reload({ only: ["todos"] });
+ },
+ }
+ );
+ };
+
+ const requestRemoveTag = (todoId, tagId) => {
+ Inertia.put(
+ route("todos.removeTag", { id: todoId }),
+ {
+ tagId,
+ },
+ {
+ onFinish: (visit) => {
+ Inertia.reload({ only: ["todos"] });
+ },
+ }
+ );
+ };
+
return (
<Authenticated
auth={props.auth}
@@ -169,9 +213,9 @@ export default function Dashboard(props) {
)}
</Grid>
</form>
- <Box sx={{ width: "100%", maxWidth: 360 }}>
- <List>
- {todoList.map((item) => (
+ <List>
+ {todoList.map((item) => (
+ <Stack direction='row' spacing={1} alignItems='center'>
<ListItem
button
key={item.id}
@@ -223,9 +267,38 @@ export default function Dashboard(props) {
</ListItemButton>
)}
</ListItem>
- ))}
- </List>
- </Box>
+ {item.tags.map((tag) => (
+ <Chip
+ label={tag.name}
+ onDelete={() => requestRemoveTag(item.id, tag.id)}
+ />
+ ))}
+ {!item.tagging ? (
+ <Chip
+ label='Add Tag'
+ variant='outlined'
+ onClick={() => toggleTagging(item.id)}
+ />
+ ) : (
+ <Chip
+ component={() => (
+ <TextField
+ autoFocus
+ placeholder={item.name}
+ variant='standard'
+ onBlur={() => toggleTagging(item.id)}
+ onKeyDown={(e) => {
+ if (e.key == "Enter") {
+ requestAddTag(item.id, e.target.value);
+ }
+ }}
+ />
+ )}
+ />
+ )}
+ </Stack>
+ ))}
+ </List>
</Container>
</Authenticated>
);